package com.tangsm.spring.boot.validation.util;

import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.SocketTimeoutException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;

/**
 * HttpClient 工具类（支持http、https）
 * <p>https，跳过证书校验，即信任所有证书</p>
 * <p>简单就好，无需过度设计</p>
 *
 * @author tangyb 2022/01/07 15:00
 */
public class HttpClientUtils {

    private static final Logger log = LoggerFactory.getLogger(HttpClientUtils.class);

    /**
     * 多个执行线程的连接池
     */
    private final static PoolingHttpClientConnectionManager CONNECTION_MANAGER;
    /**
     * 连接超时毫秒 ps：表示建立连接的超时时间
     */
    private final static int CONNECT_TIMEOUT = 20000;

    /**
     * 传输超时毫秒 ps：表示数据传输处理时间
     */
    private final static int SOCKET_TIMEOUT = 20000;

    /**
     * 连接请求超时时间毫秒
     */
    private final static int CONNECTION_REQUEST_TIMEOUT = 20000;

    /**
     * 线程池的最大连接数
     * <p>最大连接数 >= 路由连接数 * 路由数</p>
     */
    private final static int MAX_TOTAL = 200;

    /**
     * 每个路由默认最大连接数
     * <p>所有路由连接数总和不会超过线程池的最大连接数</p>
     */
    private final static int CONNECT_DEFAULT_ROUTE = 200;

    static {
        // HttpClientConnection池
        CONNECTION_MANAGER = new PoolingHttpClientConnectionManager(getRegistry());
        // 整个连接池最大连接数
        CONNECTION_MANAGER.setMaxTotal(MAX_TOTAL);
        // 可用空闲连接过期时间,重用空闲连接时会先检查是否空闲时间超过这个时间，如果超过，释放socket重新建立
        // connectionManager.setValidateAfterInactivity(50000);
        // 每路由最大连接数，默认值是20
        CONNECTION_MANAGER.setDefaultMaxPerRoute(CONNECT_DEFAULT_ROUTE);
    }

    /**
     * http get url请求
     *
     * @param url 请求地址
     * @return result 响应结果
     */
    public static String httpGet(String url) {
        log.info("请求地址：{}", url);

        HttpGet httpGet = new HttpGet(url);

        return getResult(httpGet);
    }

    /**
     * http get url map参数请求
     *
     * @param url    请求地址
     * @param params 请求参数
     * @return result 响应结果
     */
    public static String httpGet(String url, Map<String, Object> params) {
        log.info("请求地址：{}", url);
        log.info("请求参数：{}", params);
        String result = "";
        try {
            URIBuilder ub = new URIBuilder(url);

            // 设置参数
            List<NameValuePair> pairs = covertParams(params);
            ub.setParameters(pairs);

            HttpGet httpGet = new HttpGet(ub.build());

            result = getResult(httpGet);
        } catch (URISyntaxException e) {
            log.error("字符串解析为URI引用异常", e);
        }
        return result;
    }

    /**
     * http get header定制化 + url map参数请求
     *
     * @param url     请求地址
     * @param headers 请求头
     * @param params  请求参数
     * @return result 响应结果
     */
    public static String httpGet(String url, Map<String, Object> headers, Map<String, Object> params) {
        log.info("请求地址：{}", url);
        log.info("请求头：{}", headers);
        log.info("请求参数：{}", params);
        String result = "";
        try {
            // URI实例的构建器
            URIBuilder ub = new URIBuilder(url);

            // 设置参数
            List<NameValuePair> pairs = covertParams(params);
            ub.setParameters(pairs);

            HttpGet httpGet = new HttpGet(ub.build());

            // 设置header参数
            if (headers != null && headers.size() > 0) {
                for (Map.Entry<String, Object> param : headers.entrySet()) {
                    httpGet.addHeader(param.getKey(), String.valueOf(param.getValue()));
                }
            }

            result = getResult(httpGet);
        } catch (URISyntaxException e) {
            log.error("无法将字符串解析为URI引用异常", e);
        }
        return result;
    }

    /**
     * http post url请求
     *
     * @param url 请求地址
     * @return result 响应结果
     */
    public static String httpPost(String url) {
        log.info("请求地址：{}", url);

        HttpPost httpPost = new HttpPost(url);

        return getResult(httpPost);
    }

    /**
     * http post map参数请求
     *
     * @param url    请求地址
     * @param params 请求参数
     * @return result 响应结果
     */
    public static String httpPost(String url, Map<String, Object> params) {
        log.info("请求地址：{}", url);
        log.info("请求参数：{}", params);
        HttpPost httpPost = new HttpPost(url);

        // 封装url参数
        List<NameValuePair> pairs = covertParams(params);
        httpPost.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8));

        return getResult(httpPost);
    }

    /**
     * http post json参数请求
     *
     * @param url        请求地址
     * @param jsonParams 请求参数（Json格式字符串）
     * @return result 响应结果
     */
    public static String httpPost(String url, String jsonParams) {
        log.info("请求地址：{}", url);
        log.info("请求参数：{}", jsonParams);
        HttpPost httpPost = new HttpPost(url);

        // 设置header参数内容类型
        httpPost.setHeader("Content-Type", "application/json; charset=utf-8");

        // 设置请求body参数
        httpPost.setEntity(new StringEntity(jsonParams, StandardCharsets.UTF_8));

        return getResult(httpPost);
    }

    /**
     * http post xml参数请求
     *
     * @param url     请求地址
     * @param xmlData 请求参数
     * @return result 响应结果
     */
    public static String httpPostXml(String url, String xmlData) {
        log.info("请求地址：{}", url);
        log.info("请求参数：{}", xmlData);

        HttpPost httpPost = new HttpPost(url);

        // 设置header参数内容类型
        httpPost.addHeader("Content-Type", "text/xml");

        // 设置请求body参数
        httpPost.setEntity(new StringEntity(xmlData, StandardCharsets.UTF_8));

        return getResult(httpPost);
    }

    /**
     * http post headers定制化 + map参数请求
     *
     * @param url     请求地址
     * @param headers 请求头
     * @param params  请求参数
     * @return result 响应结果
     */
    public static String httpPost(String url, Map<String, Object> headers, Map<String, Object> params) {
        log.info("请求地址：{}", url);
        log.info("请求头：{}", headers);
        log.info("请求参数：{}", params);
        String result = "";
        try {
            HttpPost httpPost = new HttpPost(url);

            // 设置header参数
            setHeaders(headers, httpPost);

            // 封装url参数
            List<NameValuePair> pairs = covertParams(params);
            httpPost.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8.name()));

            result = getResult(httpPost);
        } catch (UnsupportedEncodingException e) {
            log.error("字符集异常", e);
        }
        return result;
    }

    /**
     * http post headers定制化 + json请求
     *
     * @param url        请求地址
     * @param headers    请求头
     * @param jsonParams 参数（JSON格式）
     * @return result 响应结果
     */
    public static String httpPost(String url, Map<String, Object> headers, String jsonParams) {
        log.info("请求地址：{}", url);
        log.info("请求头：{}", headers);
        log.info("请求参数：{}", jsonParams);
        String result = "";
        try {
            HttpPost httpPost = new HttpPost(url);

            // 设置header参数
            setHeaders(headers, httpPost);
            httpPost.setHeader("Content-Type", "application/json; charset=utf-8");

            // 设置body参数
            StringEntity se = new StringEntity(jsonParams, StandardCharsets.UTF_8);
            httpPost.setEntity(se);

            result = getResult(httpPost);
        } catch (UnsupportedCharsetException e) {
            log.error("字符集异常", e);
        }
        return result;
    }

    /**
     * 获取BasicAuthorization基础验证授权
     *
     * @param username 授权用户
     * @param password 授权密码
     * @return 授权信息
     */
    public static String getBasicAuthenticationHeader(String username, String password) {
        String valueToEncode = String.format("%s:%s", username, password);
        return "Basic ".concat(Base64.getEncoder().encodeToString(valueToEncode.getBytes()));
    }

    /**
     * 获取HTTPClient注册器
     *
     * @return HTTPClient注册器
     */
    private static Registry<ConnectionSocketFactory> getRegistry() {
        Registry<ConnectionSocketFactory> registry = null;

        try {
            registry = RegistryBuilder.<ConnectionSocketFactory>create()
                    .register("http", new PlainConnectionSocketFactory())
                    .register("https", getSslFactory())
                    .build();
        } catch (Exception e) {
            log.error("获取 HTTPClient注册器失败", e);
        }

        return registry;
    }

    /**
     * 获取HTTPS SSL连接工厂
     * <p>跳过证书校验，即信任所有证书</p>
     *
     * @return HTTPS连接工厂
     * @throws Exception 异常信息
     */
    private static SSLConnectionSocketFactory getSslFactory() throws Exception {
        // 设置HTTPS SSL证书信息，跳过证书校验，即信任所有证书请求HTTPS
        SSLContextBuilder sslBuilder = new SSLContextBuilder()
                .loadTrustMaterial(null, (chain, authType) -> true);

        // 获取HTTPS SSL证书连接上下文
        SSLContext sslContext = sslBuilder.build();

        // 获取HTTPS连接工厂
        return new SSLConnectionSocketFactory(sslContext,
                new String[]{"TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3"},
                null,
                NoopHostnameVerifier.INSTANCE);
    }

    /**
     * 通过连接池获取HttpClient
     *
     * @return HttpClient
     */
    private static CloseableHttpClient getHttpClient() {
        // 请求配置项：设置传输超时、连接超时、从线程池获取连接超时
        RequestConfig config = RequestConfig.custom()
                .setSocketTimeout(SOCKET_TIMEOUT)
                .setConnectTimeout(CONNECT_TIMEOUT)
                .setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT)
                .build();

        // 返回初始化HttpClient
        return HttpClients.custom()
                .setMaxConnPerRoute(CONNECT_DEFAULT_ROUTE)
                .disableConnectionState()
                .setDefaultRequestConfig(config)
                .setConnectionManager(CONNECTION_MANAGER)
                .build();
    }

    /**
     * 封装url参数
     * <p>Map转换成NameValuePair List集合</p>
     *
     * @param params 请求参数
     * @return pairs NameValuePair List集合
     */
    private static List<NameValuePair> covertParams(Map<String, Object> params) {
        List<NameValuePair> pairs = new ArrayList<>();

        // 参数为空
        if (params == null || params.size() == 0) {
            return pairs;
        }

        for (Map.Entry<String, Object> param : params.entrySet()) {
            pairs.add(new BasicNameValuePair(param.getKey(), String.valueOf(param.getValue())));
        }

        return pairs;
    }

    /**
     * 设置headers参数
     *
     * @param headers  header参数
     * @param httpPost HTTP请求的实体
     */
    private static void setHeaders(Map<String, Object> headers, HttpPost httpPost) {
        if (headers != null && headers.size() > 0) {
            for (Map.Entry<String, Object> param : headers.entrySet()) {
                httpPost.setHeader(param.getKey(), String.valueOf(param.getValue()));
            }
        }
    }

    /**
     * 处理Http请求
     *
     * @param request Http请求属性对象
     * @return result 响应结果
     */
    private static String getResult(HttpRequestBase request) {
        log.info("请求地址：{}", request.getURI());

        String result = null;
        CloseableHttpClient httpClient = getHttpClient();
        CloseableHttpResponse response = null;
        try {
            // 执行 HTTP 请求
            response = httpClient.execute(request);

            // 获取此响应的消息实体
            HttpEntity entity = response.getEntity();
            // 响应状态
            StatusLine statusLine = response.getStatusLine();
            log.info("响应状态为：{}", statusLine);

            // 消息实体不为空
            if (entity != null) {
                result = EntityUtils.toString(entity, StandardCharsets.UTF_8);

                log.debug("响应内容为：{}", result);
                log.info("请求完成.");
                return result;
            }
        } catch (ConnectTimeoutException e) {
            // 连接超时异常
            log.error("连接超时异常", e);
        } catch (SocketTimeoutException e) {
            // 读取超时异常
            log.error("读取超时异常", e);
        } catch (ClientProtocolException e) {
            // 该异常通常是协议错误导致:比如构造HttpGet对象时传入协议不对(将'http'写成'htp')or响应内容不符合
            log.error("协议错误异常", e);
        } catch (ParseException e) {
            // 解析异常
            log.error("解析异常", e);
        } catch (IOException e) {
            // 该异常通常是网络原因引起的,如HTTP服务器未启动等
            log.error("网络异常", e);
        } catch (Exception e) {
            log.error("其他未知异常", e);
        } finally {
            if (response != null) {
                try {
                    response.close();
                } catch (IOException e) {
                    log.error("IO 关闭异常", e);
                }
            }
            try {
                // 虽然连接池会管理连接，但使用完关闭它仍然好习惯
                httpClient.close();
            } catch (IOException e) {
                log.error("IO 关闭异常", e);
            }
            // 释放连接
            request.releaseConnection();
        }
        return result;
    }
}